from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
from ConfigParser import ConfigParser, DEFAULTSECT, NoOptionError
from cStringIO import StringIO
from time import time as now
from optparse import OptionParser

from xmlrpclib import Fault, loads as loads_xmlrpc, dumps as dumps_xmlrpc

from config import ServerConfiguration
from config import create_random_name
from config import parse_safe_string

from config import reraise
from config import debug

from platform import getNativePlatform
NativePlatform = getNativePlatform()

class Download:
    # Todo: group order.
    class Group:
        pass

    class Request:
        @classmethod
        def parse(self, descr):
            # Multi-request.
            cfg = ConfigParser()
            cfg.readfp(StringIO(descr))

            for section in cfg.sections():
                values = dict((k, cfg.get(section, k)) for \
                              k in cfg.options(section))

                # Pre-Validation.
                if 'name' not in values:
                    # XXX Is this right?
                    if section is not DEFAULTSECT:
                        values['name'] = section

                assert 'url' in values
                yield self(**values)

        def __init__(self, **values):
            keys = self.__keys = []
            for (k, v) in values.iteritems():
                if not k.startswith('__'):
                    keys.append(k)
                    setattr(self, k, v)

            # Post-Validation.
            if 'filename' not in keys:
                self.filename = None

        def __repr__(self):
            values = ', '.join('%s: %r' % (k, getattr(self, k)) \
                               for k in self.__keys)

            return '%s {%s}' % (self.__class__.__name__, values)

    class Selector:
        class Criterium:
            def match(self, value):
                pass

        def __init__(self, criteria):
            # Todo: filter/translate only allowed query keys.
            self.criteria = criteria
        def __call__(self, downloads):
            for dnl in downloads:
                if self.match(dnl):
                    yield dnl

        def match(self, dnl):
            if self.criteria:
                for (k, c) in self.criteria.iteritems():
                    try: v = getattr(dnl, k)
                    except AttributeError:
                        return False

                    if issubclass(c.__class__, self.Criterium):
                        if not c.match(v):
                            return False

                    elif v != c:
                        return False

                return True

    DOWNLOAD_CANCELLED = -1
    DOWNLOAD_FINISHED = 0
    DOWNLOAD_INITIALIZED = 1
    DOWNLOAD_PAUSED = 2
    DOWNLOAD_ACTIVE = 3

    _state_names = {DOWNLOAD_CANCELLED   : 'Cancelled',
                    DOWNLOAD_FINISHED    : 'Finished',
                    DOWNLOAD_INITIALIZED : 'Initialized',
                    DOWNLOAD_PAUSED      : 'Paused',
                    DOWNLOAD_ACTIVE      : 'Active'}

    def __init__(self, manager, req):
        self.manager = manager
        self.req = req

        # Encapsulate the request, but activate settings:
        self.idnum = manager.create_download_id()
        self.name = req.name or manager.create_download_name()

        # Start method is separate from construction (for object serialization).
        self.state = self.DOWNLOAD_INITIALIZED
        self.pid = None

        self.start_time = now()

    def download_args(self):
        args = dict(url = self.req.url)
        if self.req.filename:
            args['filename'] = self.req.filename

        return args

    def start(self):
        if self.state in [self.DOWNLOAD_INITIALIZED]:
            self.pid = self.manager.spawn_download(self.download_args())
            self.state = self.DOWNLOAD_ACTIVE

    def cancel(self, complete = False, delete = False):
        # Abort.
        if self.state != self.DOWNLOAD_FINISHED:
            if self.state in [self.DOWNLOAD_ACTIVE, self.DOWNLOAD_PAUSED]:
                self.manager.kill_download(self.pid)

            self.state = self.DOWNLOAD_CANCELLED
            if complete:
                self.complete(delete = delete)

    def pause(self):
        if self.state == self.DOWNLOAD_ACTIVE:
            self.manager.pause_download(self.pid)
            self.state = self.DOWNLOAD_PAUSED

    def resume(self):
        if self.state == self.DOWNLOAD_PAUSED:
            self.manager.resume_download(self.pid)
            self.state = self.DOWNLOAD_ACTIVE

    def reap(self, status):
        self.state = self.DOWNLOAD_FINISHED

    def complete(self, delete = False):
        # Todo: process delete flag.
        if self.state not in [self.DOWNLOAD_FINISHED, self.DOWNLOAD_CANCELLED]:
            # State-Error.
            raise ValueError(self.state)

        self.manager.remove_download(self)

    # Description.
    def get_status(self):
        return dict(state = self._state_names[self.state],
                    duration = now() - self.start_time)

    def __repr__(self):
        return '%s (%r)' % (self.__class__.__name__, self.req)

class EventSink:
    class Dispatch:
        def __init__(self, manager, *parts):
            self.manager = manager
            self.name = self.buildEventName(parts)
        def __getattr__(self, name):
            return self.__class__(self.manager, self.name, name)
        def __call__(self, *args, **kwd):
            try: fire = self.manager.fireEvent
            except AttributeError: pass
            else: return fire(self.name, *args, **kwd)

        @classmethod
        def buildEventName(self, parts):
            parts = filter(None, parts)
            # capitalize = lambda s: s[0].upper() + s[1:]
            # return ''.join(capitalize(p) for p in parts)
            return parts and '_'.join(parts) or ''

    def __init__(self, manager = None):
        self.event = self.Dispatch(manager or self)

class PluginManager(EventSink):
    # Todo: Configuration options.
    def __init__(self, config):
        EventSink.__init__(self)
        self.plugins = []

    # Invocation Framework.
    class Outcome(Exception):
        def __init__(self, outcome):
            self.outcome = outcome
        def getOutcome(self):
            return self.outcome

    def fireEvent(self, name, *args, **kwd):
        for p in self.plugins:
            try: p.handleEvent(self, name, *args, **kwd)
            except self.Outcome, o:
                return o.getOutcome()

    @classmethod
    def eventOutcome(self, *args, **kwd):
        raise self.Outcome(*args, **kwd)

    # Installation Framework.
    def installPlugin(self, plugin):
        # Todo: Delegate into plugins??
        if plugin not in self.plugins:
            self.plugins.append(plugin)
            plugin.install(self)

    def uninstallPlugin(self, plugin):
        if plugin in self.plugins:
            self.plugins.remove(plugin)
            plugin.uninstall(self)

    def reloadPlugin(self, plugin):
        pass

    class Plugin:
        '''
        class Custom(PluginManager.Plugin):
            def parse_request(self, descr):
                return self.eventOutcome(True)
            def create_download(self, req):
                return self.eventOutcome(True)
            def download_created(self, dnl):
                return self.eventOutcome(True)
            def start_download(self, dnl):
                return self.eventOutcome(True)
            def resume_download(self, dnl):
                return self.eventOutcome(True)
            def download_status(self, dnl):
                return self.eventOutcome(True)
        '''
        def handleEvent(self, manager, name, *args, **kwd):
            action = getattr(self, name, None)
            if callable(action):
                return action(manager, *args, **kwd)

        def eventOutcome(self, *args, **kwd):
            return PluginManager.eventOutcome(*args, **kwd)

        def install(self, manager):
            return True
        def uninstall(self, manager):
            return True

    # RPC Methods.
    def install(self):
        pass
    def uninstall(self):
        pass
    def reload(self):
        pass

class DownloadManager(NativePlatform, PluginManager):
    # Todo: Download persistance.
    # Todo: Permissive Capabilities.
    # Todo: SIGCHLD reaper.

    NAME_LENGTH = 32
    NAME_CHARACTERS = 'abcdef0123456789'

    def __init__(self, config):
        PluginManager.__init__(self, config)
        NativePlatform.__init__(self, config)

        self.downloadsByName   = {}
        self.downloadsByIdnum  = {}
        self.downloadsByPID    = {}

    @property
    def downloads(self):
        return self.downloadsByIdnum.itervalues()

    def create_download_id(self):
        downloads = [dnl.idnum for dnl in self.downloads]
        return max(downloads) + 1 if downloads else 0
    def create_download_name(self):
        while True:
            name = create_random_name(self.NAME_LENGTH, self.NAME_CHARACTERS)
            if name not in self.downloadsByName:
                return name

    def get_download(self, name):
        if type(name) is int:
            return self.downloadsByIdnum[name]
        if type(name) is str:
            return self.downloadsByName[name]

    def remove_download(self, dnl):
        del self.downloadsByIdnum[dnl.idnum]
        del self.downloadsByName[dnl.name]
        del self.downloadsByPID[dnl.pid]

    def reap(self, signr, stack_frame):
        # If this fails, it was never meant to be.
        for (pid, status) in self.cleanupChildProcesses():
            dnl = self.downloadsByPID.get(pid)
            if dnl is not None:
                dnl.reap(status)
                self.event.server.reap.process(dnl, status, stack_frame)

    # XMLRPC Methods.
    def download(self, descr):
        request_list = self.event.parse.request(descr)
        if request_list is None:
            request_list = Download.Request.parse(descr)
            request_list = list(request_list)

            rql = self.event.prepare.request(request_list)
            if rql is not None:
                request_list = rql

        ids = []
        for req in request_list:
            if req.name in self.downloadsByName:
                raise NameError(req.name)

            dnl = self.event.create.download(req)
            if dnl is None:
                dnl = Download(self, req)

            self.downloadsByName[dnl.name] = dnl
            self.downloadsByIdnum[dnl.idnum] = dnl

            if not self.event.start.download(dnl):
                dnl.start()
                self.downloadsByPID[dnl.pid] = dnl

            self.event.download.created(dnl)
            ids.append(dnl.idnum)

        return ids

    def cancel(self, name, complete = None, delete = None):
        dnl = self.get_download(name)
        if not self.event.cancel.download(dnl):
            dnl.cancel(complete = complete, delete = delete)

    def pause(self, name):
        dnl = self.get_download(name)
        if not self.event.pause.download(dnl):
            dnl.pause()
    def resume(self, name):
        dnl = self.get_download(name)
        if not self.event.resume.download(dnl):
            dnl.pause()

    def status(self, name):
        dnl = self.get_download(name)
        return self.event.download.status(dnl) or dnl.get_status()
    def complete(self, name, delete = False):
        # Finishes the download by removing it from manager.
        dnl = self.get_download(name)
        self.event.download.complete(dnl)
        if dnl.complete(delete = delete):
            self.remove_download(dnl)

    def query(self, criteria):
        # Return download ids matching criteria.
        select = Download.Selector(criteria)
        return [dnl.idnum for dnl in select(self.downloads)]

    def restart(self, name):
        pass

class ServerControl(EventSink):
    SERVER_CLOSED   = 0
    SERVER_CLOSING  = 2
    SERVER_STOPPED  = 1
    SERVER_STOPPING = 3
    SERVER_RUNNING  = 4

    _state_names = {SERVER_CLOSED   : 'Closed'  ,
                    SERVER_CLOSING  : 'Closing' ,
                    SERVER_STOPPED  : 'Stopped' ,
                    SERVER_STOPPING : 'Stopping',
                    SERVER_RUNNING  : 'Running' }

    _state_codes = {'Closed'   : SERVER_CLOSED  ,
                    'Closing'  : SERVER_CLOSING ,
                    'Stopped'  : SERVER_STOPPED ,
                    'Stopping' : SERVER_STOPPING,
                    'Running ' : SERVER_RUNNING }

    def __init__(self):
        EventSink.__init__(self, self)

    def serve_cooperatively(self):
        self.set_state(self.SERVER_RUNNING)
        while True:
            state = self.get_state()
            if state != self.SERVER_RUNNING:
                break

            # Todo: implement long timeouts (for catching server shutdowns.)
            try: self.handle_request()
            except NativePlatform.SelectError, e:
                if NativePlatform.isInterruptError(e):
                    pass

            except KeyboardInterrupt:
                self.set_state(self.SERVER_STOPPING)
                break

        if state == self.SERVER_CLOSING:
            self.server_shutdown()
            self.set_state(self.SERVER_CLOSED)
        else:
            self.set_state(self.SERVER_STOPPED)

    def get_state(self):
        try: return self.__state
        except AttributeError:
            return self.SERVER_STOPPED

    def set_state(self, state):
        state = self._changeState(state)
        self.__state = int(state)

    def _changeState(self, state):
        # Operate EventSink.
        change = self.event.server.state.change(self._state_names[state])
        if change is not None:
            change = self._state_codes.get(change)
            if change is not None:
                state = change

        return state

    # RPC Methods.
    def srv_start(self):
        if self.get_state() == self.SERVER_STOPPED:
            nth(self.serve_cooperatively, ())

    def srv_stop(self):
        self.set_state(self.SERVER_STOPPING)

    def srv_shutdown(self):
        self.set_state(self.SERVER_CLOSING)

    def srv_state(self):
        return self._state_names[self.get_state()]

class XMLRPC(SimpleXMLRPCServer):
    # Customized server generates better faultString (for debugging purposes)
    # and also defines unregister_function, RequestHandler (for plugin dynamicity).

    from SimpleHTTPServer import SimpleHTTPRequestHandler
    class RequestHandler(SimpleXMLRPCRequestHandler, SimpleHTTPRequestHandler):
        def handle(self):
            # Todo: implement bypass (or WSGI!)
            self.server.event.handle.server.request(self)
            return SimpleXMLRPCRequestHandler.handle(self)

    def __init__(self, config):
        SimpleXMLRPCServer.__init__(self, (config.address, config.port),
                                    allow_none = True, logRequests = False,
                                    requestHandler = self.RequestHandler)

    def _marshaled_dispatch(self, data, dispatch_method = None):
        # debug()
        try:
            params, method = loads_xmlrpc(data)

            if dispatch_method is not None:
                response = dispatch_method(method, params)
            else:
                response = self._dispatch(method, params)

            response = (response,)
            response = dumps_xmlrpc(response, methodresponse=1,
                                       allow_none=self.allow_none, encoding=self.encoding)

        except Fault, fault:
            response = dumps_xmlrpc(fault, allow_none=self.allow_none,
                                    encoding=self.encoding)

        except:
            response = dumps_xmlrpc(Fault(1, self.getFaultString()),
                                    allow_none=self.allow_none, encoding=self.encoding)

        return response

    def getFaultString(self):
        ##    import sys
        ##    return "%s:%s" % (sys.exc_type, sys.exc_value)

        from traceback import format_exc
        return format_exc()

    def unregister_function(self, name):
        del self.funcs[name]

class DownloadServer(XMLRPC, ServerControl, DownloadManager):
    def __init__(self, config):
        XMLRPC.__init__(self, config)
        DownloadManager.__init__(self, config)
        self.register_download_functions()

    def register_download_functions(self):
        self.register_function(self.download    , 'download.initiate'   )
        self.register_function(self.cancel      , 'download.cancel'     )
        self.register_function(self.pause       , 'download.pause'      )
        self.register_function(self.resume      , 'download.resume'     )

        self.register_function(self.status      , 'download.status'     )
        self.register_function(self.complete    , 'download.complete'   )

        self.register_function(self.query       , 'download.query'      )
        self.register_function(self.restart     , 'download.restart'    )

        self.register_function(self.srv_state   , 'server.status'       )
        self.register_function(self.srv_start   , 'server.start'        )
        self.register_function(self.srv_shutdown, 'server.shutdown'     )
        self.register_function(self.srv_stop    , 'server.stop'         )

        self.register_function(self.install     , 'plugin.install'      )
        self.register_function(self.uninstall   , 'plugin.uninstall'    )
        self.register_function(self.reload      , 'plugin.reload'       )

# Builtin Plugins.
class Logging(PluginManager.Plugin):
    # Formula.
    def getTimeString(self):
        from datetime import datetime
        now = datetime.today()
        return '%s/%s/%s %s:%s:%s' % (now.month, now.day, now.year,
                                      now.hour, now.minute, now.second)

    def log(self, msg, level = 0):
        print '[%s] :: %s' % (self.getTimeString(), msg)

    # Download Events.
    def parse_request(self, manager, descr):
        descr = '\n    '.join(descr.split('\n'))
        self.log('Parsing Description:\n  {\n    %s\n  }' % descr)
    def prepare_request(self, manager, req):
        self.log('Preparing Request: {%r}' % req)
    def create_download(self, manager, req):
        self.log('Creating Download: {%r}'% req)
    def download_created(self, manager, dnl):
        self.log('Download Created: {%r}' % dnl)
    def start_download(self, manager, dnl):
        self.log('Starting Download: {%r}' % dnl)
    def cancel_download(self, manager, dnl):
        self.log('Cancelling Download: {%r}' % dnl)
    def pause_download(self, manager, dnl):
        self.log('Pausing Download: {%r}' % dnl)
    def resume_download(self, manager, dnl):
        self.log('Resuming Download: {%r}' % dnl)
    def download_status(self, manager, dnl):
        self.log('Download Status: {%r}' % dnl)

    # Server Events.
    def server_state_change(self, manager, state):
        self.log('Server State Change: %s' % state)

    ##    def handle_server_request(self, manager, request):
    ##        self.log('Handling Request: {%r}' % request)

    def server_reap_process(self, manager, dnl, status, stack_frame):
        self.log('Server Reap Process [#%d]: %s' % (status, stack_frame))

class Debugging(PluginManager.Plugin):
    def install(self, manager):
        self.manager = manager
        manager.register_function(self.evaluate, 'debug.evaluate')
    def uninstall(self, manager):
        self.manager = None
        manager.unregister_function('debug.evaluate')

    _ns_locals = {}
    def evaluate(self, *args, **kwd):
        self._ns_locals['manager'] = self.manager

        from downmgr.config import evaluate
        return evaluate(self._ns_locals, *args, **kwd)

    def handle_server_request(self, manager, request):
        self._ns_locals['request'] = request

# Front End.
def parse_cmdln(argv = None):
    parser = OptionParser()

    # Server Options.
    parser.add_option('-C', '--config-file')
    parser.add_option('--bind-address')
    parser.add_option('-p', '--port')
    parser.add_option('--wget')

    # Builtin Plugins.
    parser.add_option('--logging', action = 'store_true')
    parser.add_option('--debugging', action = 'store_true')

    return parser.parse_args(argv)

def doRunServer(*argv):
    (options, args) = parse_cmdln(list(argv))

    config = ServerConfiguration()
    if options.bind_address:
        config.address = options.bind_address
    if options.port:
        config.port = options.port
    if options.wget:
        config.wgetbin = options.wget

    global dnl_server
    dnl_server = DownloadServer(config)

    if options.logging:
        dnl_server.installPlugin(Logging())
    if options.debugging:
        dnl_server.installPlugin(Debugging())

    dnl_server.serve_cooperatively()
